﻿#region using

using System;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Web.UI;
using Pentia.Linq.Mapping;

#endregion

namespace Pentia.Linq
{
  internal class SitecoreQueryParser
  {
    private readonly IQueryContext _contextData;
    private readonly Expression _expression;
    private readonly QueryBuilder _queryBuilder = new QueryBuilder();
    private Type _returnType;

    public SitecoreQueryParser(IQueryContext contextData, Expression expression)
    {
      _expression = expression;
      _contextData = contextData;
    }

    public IQueryContext ContextData
    {
      get { return _contextData; }
    }

    public Type ReturnType
    {
      get { return _returnType; }
    }

    public Expression Expression
    {
      get { return _expression; }
    }

    public QueryBuilder QueryBuilder
    {
      get { return _queryBuilder; }
    }

    public TextWriter Log { get; set; }

    private Opcode ParsedQuery { get; set; }

    public Query.Query Parse()
    {
      //Determine where to start the query by extracting the root path 
      Step queryRoot = null; //This points to the root (\) of the path
      Step predicateStep = null; //This points to the step where the predicate will be attached
      if (ContextData.RootItem != null)
      {
        //Start from the RootItem parent, we apply the actual RootItem later
        Item currentItem = ContextData.RootItem;
        //Traverse up through the tree, starting from the root of the query
        do
        {
          //Create a step from the current item and set the previous item's step as the NextStep
          Step nextItemStep = queryRoot;
          queryRoot = QueryBuilder.Children(QueryBuilder.ItemElement(currentItem.Name, null));
          if (nextItemStep != null)
            queryRoot.NextStep = nextItemStep;
          //If this is the first step found, this will be the root for our query
          if (predicateStep == null)
            predicateStep = queryRoot;
          currentItem = currentItem.Parent;
        } while (currentItem != null);
        Step nextStep = queryRoot;
        queryRoot = QueryBuilder.Root();
        queryRoot.NextStep = nextStep;
      }
      else
      {
        queryRoot = QueryBuilder.Root();
        predicateStep = queryRoot;
      }

      //Call recursive parsing
      Predicate predicate = Parse(Expression);

      if (ContextData.TemplateID != null)
      {
        Opcode templatePredicate = QueryBuilder.Predicate(QueryBuilder.Equals(QueryBuilder.FieldElement("@templateid"), QueryBuilder.Literal(ContextData.TemplateID)));
        if (predicate == null)
          predicate = QueryBuilder.Predicate(templatePredicate);
        else
          predicate = QueryBuilder.Predicate(QueryBuilder.And(templatePredicate, predicate));
      }

      ElementBase item = QueryBuilder.ItemElement(ContextData.ItemName ?? "*", predicate);
      switch (ContextData.AxisType)
      {
        case Axis.Children:
          predicateStep.NextStep = QueryBuilder.Children(item);
          break;
        case Axis.Ancestor:
          predicateStep.NextStep = QueryBuilder.Ancestor(item);
          break;
        case Axis.AncestorOrSelf:
          predicateStep.NextStep = QueryBuilder.AncestorOrSelf(item);
          break;
        case Axis.Descendant:
          predicateStep.NextStep = QueryBuilder.Descendants(item);
          break;
        case Axis.DescendantOrSelf:
          predicateStep.NextStep = QueryBuilder.DescendantsOrSelf(item);
          break;
        case Axis.Following:
          predicateStep.NextStep = QueryBuilder.Following(item);
          break;
        case Axis.Preceding:
          predicateStep.NextStep = QueryBuilder.Preceding(item);
          break;
      }

      ParsedQuery = ParsedQuery != null ? QueryBuilder.Add(ParsedQuery, queryRoot) : queryRoot;
      WriteToLog(ParsedQuery.Print);


      Query.Query query = new Query.Query(ParsedQuery);
      return query;
    }

    private Predicate Parse(Expression expression)
    {
      //Method calls (Where, Select etc.)
      if (expression is MethodCallExpression)
      {
        MethodCallExpression methodCall = (MethodCallExpression) expression;
        if (methodCall.Method.DeclaringType != typeof (Queryable))
          throw new NotSupportedException("Cannot use methods on non-queryable types");

        //HINT: See http://msdn.microsoft.com/en-us/library/aa479865.aspx#linqprojec_topic7
        switch (methodCall.Method.Name)
        {
          case "Where":
            // mc points to a Queryable::Where(source, predicate) function
            // parameter index 1 points to a predicate of type Func(TSource, bool)
            // otherwise fail
            Type expressionFunc = methodCall.Method.GetParameters()[1].ParameterType.GetGenericArguments()[0];
            Predicate predicate = null;
            if (expressionFunc.GetGenericArguments().Length == 2)
              predicate = ParsePredicate((LambdaExpression) ((UnaryExpression) methodCall.Arguments[1]).Operand);
            else
              throw new NotSupportedException(String.Format("Unsupported predicate expression in Where {0}",
                                                            expressionFunc.FullName));
            return predicate;
            break;
          case "Select":
            //TODO: Implement projection, but to what?
            throw new NotImplementedException(String.Format("Not implemented method {0}", methodCall.Method.Name));
            break;
          case "Take":
            //TODO: Implement result restrictions
            throw new NotImplementedException(String.Format("Not implemented method {0}", methodCall.Method.Name));
            break;
          case "First":
            //TODO: Implement result restrictions
            throw new NotImplementedException(String.Format("Not implemented method {0}", methodCall.Method.Name));
            break;
          default:
            throw new NotSupportedException(String.Format("Unsupported method {0}", methodCall.Method.Name));
        }
      }
      if (expression is ConstantExpression)
      {
        //Read the type we are interested in
        _returnType = (expression.Type.GetGenericArguments()[0]);

        WriteToLog("Found return type {0}", _returnType);
        //We ignore constant expressions - this is the from clause

        return null;
      }
      throw new NotSupportedException(String.Format("Unsupported expression type '{0}' at this position",
                                                    expression.Type.Name));
    }

    private void WriteToLog(string message, params object[] formatStrings)
    {
      if (Log != null)
        Log.WriteLine(String.Format(message, formatStrings));
    }

    private void WriteToLog(Action<HtmlTextWriter> print)
    {
      if (Log != null)
        print(new HtmlTextWriter(Log));
    }

    private Predicate ParsePredicate(LambdaExpression expression)
    {
      Opcode predicateContent = ParsePredicateContent(expression.Body, expression);
      if (predicateContent == null)
        return null;
      return QueryBuilder.Predicate(predicateContent);
    }

    private Opcode ParsePredicateContent(Expression content, Expression predicate)
    {
      BinaryExpression binaryExpression;
      UnaryExpression unaryExpression;
      MemberExpression memberExpression;
      MethodCallExpression methodCallExpression;
      ConstantExpression constantExpression;
      // The given predicate can be a binary expression containing either boolean expressions or conditions that require further parsing.
      if ((binaryExpression = content as BinaryExpression) != null)
      {
        return ParsePredicateBinary(binaryExpression);
      }
        // A unary expression will occur for boolean negation.
      else if ((unaryExpression = content as UnaryExpression) != null)
      {
        return ParsePredicateUnary(unaryExpression);
      }
      else if ((memberExpression = content as MemberExpression) != null)
      {
        return ParsePredicateMember(memberExpression);
      }
      else if ((methodCallExpression = content as MethodCallExpression) != null)
      {
        return ParsePredicateMethodCall(methodCallExpression);
      }
        // Constant values are possible if the user writes clauses like "1 == 1" in the query's where predicate.
      else if ((constantExpression = content as ConstantExpression) != null)
      {
        return ParsePredicateConstant(constantExpression, predicate);
      }

      // Should not occur
      throw new NotSupportedException(String.Format("Unsupported expression {0} in a predicate", content));
    }

    private Opcode ParsePredicateConstant(ConstantExpression constant, Expression predicateExpression)
    {
      //TODO: Check if the constant is part of a Sitecore field comparison, if so convert the .NET types to the correct Sitecore datatype
      if (constant.Value is Boolean)
        return QueryBuilder.BooleanValue((Boolean) constant.Value);
      if (constant.Value is Int32)
        return QueryBuilder.Number((Int32) constant.Value);
      if (constant.Value is String)
        return QueryBuilder.Literal((String) constant.Value);
      //TODO: Is this the correct way, or should we support more datatypes?
      throw new NotSupportedException(string.Format("Unsupported constant {0} in query", constant.Value));
    }

    private Opcode ParsePredicateMethodCall(MethodCallExpression expression)
    {
      //TODO:Handle Contains(IQueryable<IStandardTemplate>) as [@id=*/@id]
      throw new NotImplementedException();
    }

    private Opcode ParsePredicateMember(MemberExpression expression)
    {
      Object[] attributes = expression.Member.GetCustomAttributes(typeof (SitecorePropertyAttribute), true);
      if (attributes.Length == 1)
      {
        String propertyName = ((SitecorePropertyAttribute) attributes[0]).Name;
        return QueryBuilder.FieldElement(string.Format("@{0}", propertyName));
      }

      throw new NotSupportedException(
        string.Format(
          "Unsupport member {0}. Members must be marked with the SitecorePropertyAttribute or SitecoreFieldProperty",
          expression.Expression));
    }

    private Opcode ParsePredicateUnary(UnaryExpression expression)
    {
      throw new NotImplementedException();
    }

    private Opcode ParsePredicateBinary(BinaryExpression expression)
    {
      switch (expression.NodeType)
      {
        case ExpressionType.And:
        case ExpressionType.Or:
        case ExpressionType.AndAlso:
        case ExpressionType.OrElse:
          {
            Opcode leftPredicate = ParsePredicateContent(expression.Left, expression);
            Opcode rightPredicate = ParsePredicateContent(expression.Right, expression);
            if (leftPredicate == null && rightPredicate == null)
              return null;
            if (expression.NodeType == ExpressionType.And || expression.NodeType == ExpressionType.AndAlso)
              return QueryBuilder.And(leftPredicate, rightPredicate);
            else
              return QueryBuilder.Or(leftPredicate, rightPredicate);
          }
        default:
          {
            return ParsePredicateCondition(expression);
          }
      }
    }

    private Opcode ParsePredicateCondition(BinaryExpression expression)
    {
      Opcode leftExpression = ParsePredicateContent(expression.Left, expression);
      Opcode rightExpression = ParsePredicateContent(expression.Right, expression);
      switch (expression.NodeType)
      {
        case ExpressionType.Equal:
          return QueryBuilder.Equals(leftExpression, rightExpression);
        case ExpressionType.NotEqual:
          return QueryBuilder.Unequals(leftExpression, rightExpression);
        case ExpressionType.GreaterThan:
          return QueryBuilder.Greater(leftExpression, rightExpression);
        case ExpressionType.GreaterThanOrEqual:
          return QueryBuilder.GreaterOrEquals(leftExpression, rightExpression);
        case ExpressionType.LessThan:
          return QueryBuilder.Smaller(leftExpression, rightExpression);
        case ExpressionType.LessThanOrEqual:
          return QueryBuilder.SmallerOrEquals(leftExpression, rightExpression);
      }
      throw new NotSupportedException(string.Format("Unsupported comparison operator {0}", expression.NodeType));
    }
  }
}